namespace AgentJohnson
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using System.Text.RegularExpressions;
  using JetBrains.Annotations;
  using JetBrains.ReSharper.Feature.Services.CSharp.Bulbs;
  using Resharper.Abstractions.Actions;
  using Resharper.Abstractions.CodeDom.Comments;
  using Resharper.Abstractions.CodeDom.Expressions.InvocationExpressions;
  using Resharper.Abstractions.CodeDom.Statements.TryStatements;
  using Resharper.Abstractions.CodeDom.TypeMembers;
  using Resharper.Abstractions.Extensions.DocumentationExtensions;

  /// <summary>Defines the <see cref="DocumentExceptions"/> class.</summary>
  [ContextAction("Add xml-docs comments for uncaught exceptions [Agent Johnson]", "Document uncaught exceptions that are thrown in a called function")]
  public class DocumentExceptions : ContextActionBase
  {
    #region Constructors and Destructors

    /// <summary>Initializes a new instance of the <see cref="DocumentExceptions"/> class.</summary>
    /// <param name="provider">The provider.</param>
    public DocumentExceptions([NotNull] ICSharpContextActionDataProvider provider) : base(provider)
    {
    }

    #endregion

    #region Methods

    /// <summary>Determines whether this instance can execute the specified action context.</summary>
    /// <param name="context">The action context.</param>
    /// <returns><c>true</c> if this instance can execute the specified action context; otherwise, <c>false</c>.</returns>
    protected override bool CanExecute(CanExecuteContext context)
    {
      if (context == null)
      {
        throw new ArgumentNullException("context");
      }

      return this.GetModel(context) != null;
    }

    /// <summary>Executes the action.</summary>
    /// <param name="context">The execute context.</param>
    protected override void Execute(ExecuteContext context)
    {
      if (context == null)
      {
        throw new ArgumentNullException("context");
      }

      var model = this.GetModel(context);
      if (model == null)
      {
        return;
      }

      var code = new StringBuilder();
      foreach (var exception in model.Exceptions)
      {
        var text = Regex.Replace(exception.Description, "<paramref name=\"([^\"]*)\" />", "$1");
        code.AppendFormat("\r\n <exception cref=\"{0}\">{1}</exception>", exception.ExceptionTypeName, text);
      }

      model.Anchor.FormatComment(code);

      var comment = model.Anchor.Comment;
      if (comment != null)
      {
        code.Insert(0, comment.ToString());
      }
      else
      {
        code.Remove(0, 1);
      }

      code.Append("\nvoid foo(){}");

      var dummy = context.Factory.CreateTypeMember(code.ToString());
      if (dummy == null)
      {
        return;
      }

      var newComment = dummy.Comment;
      if (newComment != null)
      {
        model.Anchor.Comment = newComment;
      }

      context.Formatter.Format(model.Anchor);
    }

    /// <summary>Gets the text.</summary>
    /// <returns>Returns the text.</returns>
    protected override string GetText()
    {
      return "Add xml-docs comments for uncaught exceptions [Agent Johnson]";
    }

    /// <summary>Gets the invoked exceptions.</summary>
    /// <param name="invocationExpression">The invocation expression.</param>
    /// <returns>Returns the invoked exceptions.</returns>
    private IEnumerable<DocumentedException> GetInvokedExceptions([NotNull] InvocationExpression invocationExpression)
    {
      if (invocationExpression == null)
      {
        throw new ArgumentNullException("invocationExpression");
      }

      var declaredElement = invocationExpression.InvokedElement;
      if (declaredElement != null)
      {
        return declaredElement.DocumentedExceptions();
      }

      return Enumerable.Empty<DocumentedException>();
    }

    /// <summary>Gets the model.</summary>
    /// <param name="context">The context.</param>
    /// <returns>Returns the model.</returns>
    private Model GetModel([NotNull] ProviderContext context)
    {
      if (context == null)
      {
        throw new ArgumentNullException("context");
      }

      var invocationExpression = context.GetSelectedInvocationExpression();
      if (invocationExpression == null)
      {
        return null;
      }

      var exceptions = this.GetInvokedExceptions(invocationExpression).ToList();
      if (exceptions.Count == 0)
      {
        return null;
      }

      this.RemoveCaughtExceptions(exceptions, invocationExpression);
      if (exceptions.Count == 0)
      {
        return null;
      }

      this.RemoveDocumentedExceptions(exceptions, invocationExpression);
      if (exceptions.Count == 0)
      {
        return null;
      }

      var anchor = context.GetSelectedTypeMember();
      if (anchor == null)
      {
        return null;
      }

      return new Model
      {
        Exceptions = exceptions,
        Anchor = anchor
      };
    }

    /// <summary>Examines the catches.</summary>
    /// <param name="tryStatement">The try statement.</param>
    /// <param name="exceptions">The exceptions.</param>
    private void RemoveCaughtExceptions([NotNull] List<DocumentedException> exceptions, [NotNull] TryStatement tryStatement)
    {
      if (exceptions == null)
      {
        throw new ArgumentNullException("exceptions");
      }

      if (tryStatement == null)
      {
        throw new ArgumentNullException("tryStatement");
      }

      var catches = new List<string>();

      foreach (var clause in tryStatement.Catches)
      {
        var exceptionType = clause.ExceptionTypeName;
        if (string.IsNullOrEmpty(exceptionType))
        {
          continue;
        }

        catches.Add(exceptionType);
      }

      for (var n = exceptions.Count - 1; n >= 0; n--)
      {
        var descriptor = exceptions[n];

        if (catches.Contains(descriptor.ExceptionTypeName))
        {
          exceptions.RemoveAt(n);
        }
      }
    }

    /// <summary>Removes the caught.</summary>
    /// <param name="invocationExpression">The tree node.</param>
    /// <param name="exceptions">The exceptions.</param>
    private void RemoveCaughtExceptions([NotNull] List<DocumentedException> exceptions, [NotNull] InvocationExpression invocationExpression)
    {
      if (exceptions == null)
      {
        throw new ArgumentNullException("exceptions");
      }

      if (invocationExpression == null)
      {
        throw new ArgumentNullException("invocationExpression");
      }

      var tryStatement = invocationExpression.GetContainingTryStatement();

      while (tryStatement != null)
      {
        RemoveCaughtExceptions(exceptions, tryStatement);

        if (exceptions.Count == 0)
        {
          return;
        }

        tryStatement = tryStatement.GetContainingTryStatement();
      }
    }

    /// <summary>Removes the documented exceptions.</summary>
    /// <param name="exceptions">The exceptions.</param>
    /// <param name="invocationExpression">The invocation expression.</param>
    private void RemoveDocumentedExceptions([NotNull] List<DocumentedException> exceptions, [NotNull] InvocationExpression invocationExpression)
    {
      if (exceptions == null)
      {
        throw new ArgumentNullException("exceptions");
      }

      if (invocationExpression == null)
      {
        throw new ArgumentNullException("invocationExpression");
      }

      var typeMember = invocationExpression.GetContainingTypeMember();
      if (typeMember == null)
      {
        return;
      }

      foreach (var descriptor in typeMember.DocumentedExceptions())
      {
        for (var n = exceptions.Count - 1; n >= 0; n--)
        {
          var exception = exceptions[n];
          if (exception.ExceptionTypeName == descriptor.ExceptionTypeName)
          {
            exceptions.RemoveAt(n);
          }
        }
      }
    }

    #endregion

    /// <summary>Defines the <see cref="Model"/> class.</summary>
    private class Model
    {
      #region Properties

      /// <summary>
      /// Gets or sets the anchor.
      /// </summary>
      /// <value>The anchor.</value>
      public TypeMember Anchor { get; set; }

      /// <summary>
      /// Gets or sets the exceptions.
      /// </summary>
      /// <value>The exceptions.</value>
      [NotNull]
      public List<DocumentedException> Exceptions { get; set; }

      #endregion
    }
  }
}